Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Cosign-based signing and verification subsystem: signable adapters for profiles, rules, and network neighborhoods; CLI and docs for signing; config toggle to enable verification; verification integrated into caches and RulesWatcher; many tests and dependency updates. Changes
Sequence Diagram(s)sequenceDiagram
participant K8S as Kubernetes API
participant CACHE as Cache/Watcher
participant VERIFY as Signature Verifier
participant COSIGN as CosignAdapter
K8S->>CACHE: Fetch object
CACHE->>VERIFY: Request verification (if cfg.EnableSignatureVerification)
VERIFY->>COSIGN: Compute canonical content hash
COSIGN-->>VERIFY: ContentHash
VERIFY->>COSIGN: Decode signature from annotations
COSIGN-->>VERIFY: Signature + Certificate
VERIFY->>COSIGN: Verify signature against hash
alt verification succeeds
VERIFY-->>CACHE: Mark object verified
CACHE->>K8S: Apply/store object
else verification fails
VERIFY-->>CACHE: Report failure / increment skipped metric
CACHE->>CACHE: Skip update / record error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
pkg/signature/sign_test.go (1)
170-175: Test case name is misleading - profile has empty annotations, not nil.The test case "No annotations" uses
NewMockSignableProfilewhich initializesannotations: make(map[string]string). This creates an empty map, notnil. The test likely passes becauseDecodeSignatureFromAnnotationsfails on an empty map, but the test name suggests it's testing thenilcheck path.Consider either:
- Renaming to "Empty annotations", or
- Using
&MockSignableProfile{annotations: nil, ...}to truly test the nil case📝 Suggested clarification
{ - name: "No annotations", - profile: NewMockSignableProfile("uid", "ns", "name", map[string]string{}), + name: "Nil annotations", + profile: &MockSignableProfile{uid: "uid", namespace: "ns", name: "name", content: map[string]string{}, annotations: nil}, wantErr: true, setupSign: false, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/sign_test.go` around lines 170 - 175, The test case name "No annotations" is misleading because NewMockSignableProfile creates an empty map (annotations: make(map[string]string)); update the test to either rename the case to "Empty annotations" to reflect the actual input, or change the fixture to construct a MockSignableProfile with annotations set to nil (e.g., &MockSignableProfile{annotations: nil, ...}) so it exercises the nil-path in DecodeSignatureFromAnnotations; modify the entry that currently calls NewMockSignableProfile("uid","ns","name", map[string]string{}) to your chosen fix and update the test name accordingly.pkg/signature/verify.go (1)
29-49: Consider adding a comment explaining whyuseKeyless: trueis hardcoded for verification.Both
NewCosignAdapter(true)andNewCosignVerifier(true)use hardcodedtrue. This works because the verification uses the certificate stored in the annotations (from either keyless or key-based signing), but the reasoning isn't immediately obvious. A brief comment would improve clarity.📝 Suggested comment
+ // useKeyless=true is fine for verification since we use the certificate + // stored in the profile annotations, regardless of how the profile was signed adapter, err := NewCosignAdapter(true)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/verify.go` around lines 29 - 49, Add a short inline comment next to the hardcoded useKeyless=true calls to NewCosignAdapter(true) and NewCosignVerifier(true) explaining that verification uses the certificate embedded in the annotations (so keyless=true is safe/intentional here), and mention that signatures may come from either keyless or key-based signing but the certificate in annotations is used for verification; place this comment adjacent to the calls in VerifySignature (or the function containing adapter/ verifier creation) so future readers understand the rationale.pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)
249-264: Consider extracting verification logic into a helper method to reduce duplication.The same verification pattern is repeated three times (lines 249-264, 336-349, and 567-587). This could be extracted into a helper like
verifyProfileIfEnabled(profile *v1beta1.ApplicationProfile, name, namespace string) error.♻️ Proposed helper method
+// verifyApplicationProfile verifies the profile signature if verification is enabled. +// Returns error if verification fails, nil otherwise (including when verification is disabled). +func (apc *ApplicationProfileCacheImpl) verifyApplicationProfile(profile *v1beta1.ApplicationProfile, context string) error { + if !apc.cfg.EnableProfileVerification { + return nil + } + profileAdapter := profiles.NewApplicationProfileAdapter(profile) + if err := signature.VerifyProfile(profileAdapter); err != nil { + logger.L().Warning(context+" verification failed, skipping", + helpers.String("profile", profile.Name), + helpers.String("namespace", profile.Namespace), + helpers.Error(err)) + return err + } + logger.L().Debug(context+" verification successful", + helpers.String("profile", profile.Name), + helpers.String("namespace", profile.Namespace)) + return nil +}Then replace each verification block with:
if err := apc.verifyApplicationProfile(fullProfile, "profile"); err != nil { continue // or return, depending on context }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around lines 249 - 264, Extract the repeated signature verification block into a method on applicationProfileCache (e.g., apc.verifyApplicationProfile) that checks apc.cfg.EnableProfileVerification, constructs profiles.NewApplicationProfileAdapter(fullProfile), calls signature.VerifyProfile, logs the same Warning (including profile.Name, namespace, workloadID when available) and Debug messages, and returns an error when verification fails; then replace the three inline blocks that use fullProfile/profile.Name/namespace/workloadID and signature.VerifyProfile with a single call to apc.verifyApplicationProfile(...) and act on its error (continue or return) as before.pkg/signature/verify_test.go (1)
171-177: Test assertions are too weak - they only log but don't verify expected behavior.The test checks if issuers/identities are the same and logs, but doesn't assert the expected difference between key-based (
local) and keyless signing. Based on other tests,SignProfileWithKeyshould produce issuerlocal, whileSignProfileKeylessproduces a different issuer.💚 Proposed fix with meaningful assertions
- if sig1.Issuer == sig2.Issuer && sig1.Issuer != "" { - t.Log("Both profiles have same issuer") - } - - if sig1.Identity == sig2.Identity && sig1.Identity != "" { - t.Log("Both profiles have same identity") - } + // Key-based signing should have "local" issuer + if sig1.Issuer != "local" { + t.Errorf("Expected key-based signing issuer 'local', got '%s'", sig1.Issuer) + } + + // Keyless signing should have a non-local issuer (or at least be different in test env) + // Note: In test environment, keyless may also use "local" - adjust assertion as needed + if sig1.Identity != "local-key" { + t.Errorf("Expected key-based signing identity 'local-key', got '%s'", sig1.Identity) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/verify_test.go` around lines 171 - 177, The test currently only logs when sig1.Issuer/sig1.Identity equals sig2.*; replace those logs with concrete assertions: verify that the profile signed with SignProfileWithKey (sig1) has Issuer == "local" and that the profile signed with SignProfileKeyless (sig2) does not have Issuer == "local" (use t.Fatalf/t.Errorf to fail the test on mismatch), and similarly assert expected differences for Identity (e.g., assert sig1.Identity is set to the local identity and sig2.Identity differs or is empty per the keyless behavior). Update the checks referencing sig1, sig2, Issuer, Identity, SignProfileWithKey, and SignProfileKeyless to assert equality/inequality rather than just t.Log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/sign-profile/main.go`:
- Around line 99-103: The CLI currently requires --key (keyFile) but never uses
it when calling signature.SignProfileWithKey; update main.go so that when
useKeyless is false you load the private key from keyFile (or extend
signature.SignProfileWithKey to accept a key path) and pass the loaded key or
keyFile into the signing call; specifically, locate the validation block using
keyFile/useKeyless and the call site of signature.SignProfileWithKey and either
(A) change the call to signature.SignProfileWithKey(..., keyFile) if the API is
extended, or (B) load/parse the private key from keyFile in main.go and pass the
resulting signer/crypto.Value into signature.SignProfileWithKey so the CLI --key
flag actually affects signing.
- Around line 106-111: parseVerifyFlags (and the similar
parseExtractSignatureFlags) never initialize the profileType flag, leaving
profileType empty so the downstream check (profileType != "auto") forces
explicit parsing and yields "unknown profile type"; fix by adding a StringVar
for profileType with a sensible default (e.g., "auto") in parseVerifyFlags and
the corresponding parseExtractSignatureFlags so profileType is initialized
before the check, ensuring the branch that handles auto-detection is taken when
no explicit type is passed.
- Around line 124-128: The generate-keypair command declares publicOnly
(parseGenerateFlags) but never uses it and always writes sig.Certificate as a
single blob; update the command to explicitly output separate public and private
key files and honor the publicOnly flag: add/recognize distinct outputs (e.g.,
--output-private and --output-public or derive public file as output + ".pub"),
change the key generation/emit code that currently writes sig.Certificate to
instead write the private key when not publicOnly and always write the public
key (or only the public when publicOnly is true), and ensure the code paths
reference the existing symbols publicOnly, outputFile (or new
outputPrivate/outputPublic vars) and the created keypair (the
sig/Certificate/privateKey values) so the produced files match the flag
semantics and the generate-keypair contract.
- Around line 41-43: The bug is that when you rewrite the inferred subcommand
variable command to "sign" because the first arg starts with "-", you still pass
os.Args[2:] into the flag parsing logic which drops that first flag; update the
argument selection so that when command was set to "sign" due to a leading flag
you parse os.Args[1:] (include the original first arg), otherwise continue
parsing os.Args[2:]. Locate the code that sets command and the subsequent flag
parsing (references: the command variable and the flag parsing calls that
currently use os.Args[2:]), introduce a small helper like argsForCommand (or
inline conditional) to choose os.Args[1:] when command was rewritten and
os.Args[2:] otherwise, and apply the same fix to the other similar sites noted
(the other occurrences around the same logic).
In `@docs/signing/README.md`:
- Around line 326-329: The example Kubernetes manifest uses an invalid container
name "app容器" under the spec -> containers -> name field; replace it with a
DNS-label-compliant name (lowercase alphanumeric and hyphens) such as
"app-container", ensuring the value for the containers.name field matches the
Kubernetes naming rules (^[a-z0-9]([-a-z0-9]*[a-z0-9])?$) and contains only
ASCII lowercase letters, digits, and hyphens.
- Around line 444-448: The ordered list item header "2. **Sign Before
Applying**" (the line that begins with that text) is incorrectly numbered
duplicate; change its leading "2." to "3." so the sequence after the previous
"2." item is correct; update only the numeric prefix of the "Sign Before
Applying" list item to "3." to fix the numbering.
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 249-264: The profile verification block (guarded by
apc.cfg.EnableProfileVerification) currently skips on VerifyProfile error
without updating the existing profileState set earlier (see variable
profileState), causing the cache to show a successful state despite the profile
not being loaded; modify the error branch inside the signature.VerifyProfile
check to update profileState with the verification error and appropriate failed
status/completion fields (mirroring how user-defined profiles are handled on
verification failure), log the error, persist the updated profileState, and then
continue to the next profile to ensure consistent state reporting.
In `@pkg/signature/cosign_adapter.go`:
- Around line 106-113: The current ecdsaSign implementation builds the signature
by appending r.Bytes() and s.Bytes(), which strips leading zeros and breaks the
verifier that expects fixed 32-byte components; replace this with ASN.1 DER
encoding: add the "encoding/asn1" import, declare type ecdsaSignature struct {
R, S *big.Int }, change CosignAdapter.ecdsaSign to return
asn1.Marshal(ecdsaSignature{R: r, S: s}), and update the verification logic in
the same file (the code that currently slices the signature into fixed 32-byte
r/s) to use asn1.Unmarshal into ecdsaSignature and use the unmarshalled R and S
for verification.
- Around line 120-152: The code currently accepts an attacker-controlled public
key from sig.Certificate (used by cryptoutils.UnmarshalPEMToPublicKey) and only
checks sig.Issuer/sig.Identity when useKeyless is set, so fix by requiring and
validating an x509 certificate chain and trust anchor instead of directly
unmarshaling a raw public key: parse sig.Certificate as an x509.Certificate,
verify its chain/signature against configured trust roots (e.g., Fulcio root or
pinned CA) and certificate transparency bindings (Rekor/SCT) before extracting
the public key, ensure the extracted key matches the certificate public key used
for ecdsa.Verify, and replace the simple non-empty checks on
sig.Issuer/sig.Identity with authoritative checks that the certificate
SAN/subject matches expected pinned identities; if allowUntrusted is false,
reject any annotation-supplied/self-signed key material outright in the code
paths around cryptoutils.UnmarshalPEMToPublicKey, ecdsa.Verify, useKeyless,
sig.Issuer, and sig.Identity.
In `@pkg/signature/profiles/adapter_test.go`:
- Around line 274-278: The test currently ignores errors from
signature.SignProfileWithKey and signature.GetProfileSignature; change the calls
to capture their returned errors and fail the test if any occur (e.g., err !=
nil => t.Fatalf or require.NoError) before using apSig/spSig, and also assert
that apSig and spSig are non-nil/valid; update the calls to
signature.SignProfileWithKey(apAdapter) and
signature.SignProfileWithKey(spAdapter) and to
signature.GetProfileSignature(apAdapter)/GetProfileSignature(spAdapter) to check
and handle the returned errors immediately to avoid panics and misleading
assertions.
In `@pkg/signature/sign.go`:
- Around line 52-57: The current code replaces all profile annotations by
calling profile.SetAnnotations(annotations) with only the signature annotations
returned from adapter.EncodeSignatureToAnnotations(sig); instead, retrieve the
existing annotations (e.g., profile.GetAnnotations()), merge the signature
annotations into that map (overwriting keys from the new signature where
necessary) and then call profile.SetAnnotations(mergedAnnotations) so existing
metadata like kubescape.io/wlid and kubescape.io/status are preserved; locate
the EncodeSignatureToAnnotations call, the annotations variable, and the
profile.SetAnnotations invocation to implement this merge.
---
Nitpick comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 249-264: Extract the repeated signature verification block into a
method on applicationProfileCache (e.g., apc.verifyApplicationProfile) that
checks apc.cfg.EnableProfileVerification, constructs
profiles.NewApplicationProfileAdapter(fullProfile), calls
signature.VerifyProfile, logs the same Warning (including profile.Name,
namespace, workloadID when available) and Debug messages, and returns an error
when verification fails; then replace the three inline blocks that use
fullProfile/profile.Name/namespace/workloadID and signature.VerifyProfile with a
single call to apc.verifyApplicationProfile(...) and act on its error (continue
or return) as before.
In `@pkg/signature/sign_test.go`:
- Around line 170-175: The test case name "No annotations" is misleading because
NewMockSignableProfile creates an empty map (annotations:
make(map[string]string)); update the test to either rename the case to "Empty
annotations" to reflect the actual input, or change the fixture to construct a
MockSignableProfile with annotations set to nil (e.g.,
&MockSignableProfile{annotations: nil, ...}) so it exercises the nil-path in
DecodeSignatureFromAnnotations; modify the entry that currently calls
NewMockSignableProfile("uid","ns","name", map[string]string{}) to your chosen
fix and update the test name accordingly.
In `@pkg/signature/verify_test.go`:
- Around line 171-177: The test currently only logs when
sig1.Issuer/sig1.Identity equals sig2.*; replace those logs with concrete
assertions: verify that the profile signed with SignProfileWithKey (sig1) has
Issuer == "local" and that the profile signed with SignProfileKeyless (sig2)
does not have Issuer == "local" (use t.Fatalf/t.Errorf to fail the test on
mismatch), and similarly assert expected differences for Identity (e.g., assert
sig1.Identity is set to the local identity and sig2.Identity differs or is empty
per the keyless behavior). Update the checks referencing sig1, sig2, Issuer,
Identity, SignProfileWithKey, and SignProfileKeyless to assert
equality/inequality rather than just t.Log.
In `@pkg/signature/verify.go`:
- Around line 29-49: Add a short inline comment next to the hardcoded
useKeyless=true calls to NewCosignAdapter(true) and NewCosignVerifier(true)
explaining that verification uses the certificate embedded in the annotations
(so keyless=true is safe/intentional here), and mention that signatures may come
from either keyless or key-based signing but the certificate in annotations is
used for verification; place this comment adjacent to the calls in
VerifySignature (or the function containing adapter/ verifier creation) so
future readers understand the rationale.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
cmd/sign-profile/main.godocs/signing/README.mdgo.modpkg/config/config.gopkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/signature/annotations.gopkg/signature/cosign_adapter.gopkg/signature/interface.gopkg/signature/profiles/adapter_test.gopkg/signature/profiles/applicationprofile_adapter.gopkg/signature/profiles/seccompprofile_adapter.gopkg/signature/sign.gopkg/signature/sign_test.gopkg/signature/signer.gopkg/signature/verifier.gopkg/signature/verify.gopkg/signature/verify_test.go
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (3)
cmd/sign-profile/main.go (1)
48-53:⚠️ Potential issue | 🔴 CriticalDefault
sign-profile --...still drops the first flag.Line 87 always parses
os.Args[2:]. When command is inferred (Line 43path), the first real flag is atos.Args[1], so it gets skipped.🐛 Proposed fix
-func parseSignFlags() { +func parseSignFlags(args []string) { fs := flag.NewFlagSet("sign-profile sign", flag.ExitOnError) @@ - if err := fs.Parse(os.Args[2:]); err != nil { + if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) os.Exit(1) } @@ switch command { case "sign", "": - parseSignFlags() - if argsRewritten { - os.Args = append([]string{"sign-profile"}, os.Args[1:]...) - } + if argsRewritten { + parseSignFlags(os.Args[1:]) + } else { + parseSignFlags(os.Args[2:]) + }Also applies to: 78-88
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/sign-profile/main.go` around lines 48 - 53, The bug is that when command is inferred (switch case "sign",""), parseSignFlags() is called while os.Args still contains the original argv, then later code unshifts "sign-profile" (argsRewritten path) causing the first real flag to be dropped because parsing uses os.Args[2:]; fix by ensuring the argv mutation happens before parsing or by parsing the correct slice based on argsRewritten: move the os.Args = append([]string{"sign-profile"}, os.Args[1:]...) line to occur before calling parseSignFlags(), or change parseSignFlags() invocation to pass the correct args (use os.Args[1:] when argsRewritten is false and os.Args[2:] when a command token is present) so that parseSignFlags() sees the first flag whether command was explicit or inferred; update references to parseSignFlags, argsRewritten, and the switch handling accordingly.docs/signing/README.md (1)
450-459:⚠️ Potential issue | 🟡 MinorFix ordered-list numbering continuity in Best Practices.
Line 450 starts another
3.after Line 445 already used3.. Please renumber this sequence to keep it consistent.📝 Suggested fix
-3. **Version Your Profiles** +4. **Version Your Profiles** - Include version in metadata - Old signatures become invalid on content changes -4. **Key Management for Local Signing** +5. **Key Management for Local Signing** - Store keys in secure locations (HSM, KMS) - Rotate keys regularly - Use read-only keys for verification -5. **Audit Trail** +6. **Audit Trail** - Store signing timestamps - Track who signed what - Use GitHub Actions for audit logs🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/signing/README.md` around lines 450 - 459, The ordered list numbering in the "Best Practices" section of docs/signing/README.md is inconsistent—after the first "3." the subsequent list item "3. **Version Your Profiles**" should be renumbered to continue the sequence; update the numeric markers for "Version Your Profiles", "Key Management for Local Signing", and "Audit Trail" so they follow the previous item (e.g., 4., 5., 6.) to restore correct ordered-list continuity.pkg/signature/cosign_adapter.go (1)
202-227:⚠️ Potential issue | 🔴 CriticalStrict verification still accepts attacker-controlled certificate material.
In Line 215-223, strict mode only performs local certificate sanity checks (CA/CN/time) and does not verify certificate trust chains. Line 253-255 also treats
sig.Issuer/sig.Identityas trusted if merely non-empty. This still allows self-consistent forged profile+cert+signature bundles to pass.🔧 Suggested direction (trust-anchor + claim binding)
@@ if !allowUntrusted { - if cert.IsCA || cert.Subject.CommonName == "" { - return fmt.Errorf("invalid certificate: must not be CA and must have a valid subject") - } - - if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) { - return fmt.Errorf("certificate is not valid at this time") - } + roots, err := x509.SystemCertPool() // preferably configured Fulcio/pinned roots + if err != nil || roots == nil { + return fmt.Errorf("failed to load trust roots: %w", err) + } + if _, err := cert.Verify(x509.VerifyOptions{ + Roots: roots, + CurrentTime: time.Now(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + }); err != nil { + return fmt.Errorf("certificate chain verification failed: %w", err) + } + + if c.useKeyless { + if sig.Identity == "" || cert.Subject.CommonName != sig.Identity { + return fmt.Errorf("identity mismatch between signature metadata and certificate") + } + if sig.Issuer == "" || cert.Issuer.CommonName != sig.Issuer { + return fmt.Errorf("issuer mismatch between signature metadata and certificate") + } + } }Also applies to: 252-256
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 202 - 227, The current strict path parses sig.Certificate but only runs local sanity checks (IsCA, CommonName, dates) and later trusts sig.Issuer/sig.Identity if non-empty; fix by performing a full x509 chain verification against a configured set of trust anchors and intermediates (use x509.Verify with proper Roots and Intermediates and time check) inside the block that handles sig.Certificate when allowUntrusted is false, and require that x509.Verify succeeds; additionally, do not accept sig.Issuer or sig.Identity as authoritative unless they are derived from the verified certificate (e.g., compare sig.Issuer/sig.Identity to cert.Subject/subjectAltName or parsed OID claims from cert) so only values bound to a certificate that successfully verified are trusted.
🧹 Nitpick comments (2)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)
252-258: Extract repeated “verification-failed” state updates into one helper.These three blocks duplicate the same state-shaping logic. Centralizing it will reduce drift and keep status semantics consistent across paths.
Also applies to: 358-365, 589-596
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around lines 252 - 258, Multiple code paths repeat creating an objectcache.ProfileState with Completion:"failed", Status:"verification-failed", Name:profile.Name and Error:err and then calling apc.workloadIDToProfileState.Set(workloadID, profileState); factor this into a single helper on the receiver (e.g. func (apc *ApplicationProfileCache) setVerificationFailed(workloadID string, profile *ProfileType, err error)) that constructs the ProfileState and calls apc.workloadIDToProfileState.Set; replace the three duplicated blocks (the occurrences around the current diff and at lines ~358-365 and ~589-596) with calls to that helper to keep status shaping consistent.pkg/signature/cosign_adapter.go (1)
314-319: Use strict integer parsing for timestamp annotations.
fmt.Sscanfin Line 316 can accept partial numeric prefixes (e.g.,"123abc").strconv.ParseIntis stricter for annotation validation.🔧 Suggested refactor
import ( @@ "math/big" + "strconv" "time" @@ if timestamp, ok := annotations[AnnotationTimestamp]; ok { - var ts int64 - _, err = fmt.Sscanf(timestamp, "%d", &ts) + ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return nil, fmt.Errorf("failed to parse timestamp: %w", err) } sig.Timestamp = ts }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 314 - 319, The code currently uses fmt.Sscanf to parse the AnnotationTimestamp value (see AnnotationTimestamp handling and the local variable ts), which allows partial numeric parsing; replace that with strconv.ParseInt on the timestamp string (use base 10 and 64-bit) and assign the parsed int64 to ts, returning a wrapped error if ParseInt fails to ensure strict integer validation of the annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/config/config.go`:
- Line 184: The config key enableProfileVerification set with viper.SetDefault
isn't bound to the documented ENABLE_PROFILE_VERIFICATION env var because
viper.AutomaticEnv() doesn't map camelCase to SCREAMING_SNAKE_CASE; update the
config initialization to explicitly bind the env var (e.g., call
viper.BindEnv("enableProfileVerification", "ENABLE_PROFILE_VERIFICATION")) or
configure an env key replacer (via viper.SetEnvKeyReplacer) before
viper.AutomaticEnv() so that the enableProfileVerification key correctly picks
up ENABLE_PROFILE_VERIFICATION from the environment.
In `@pkg/signature/cosign_adapter.go`:
- Around line 241-247: The code calling asn1.Unmarshal into ecdsaSignature
ignores the returned rest and doesn't validate R/S before calling ecdsa.Verify;
change the asn1.Unmarshal call to capture the rest value and return an error if
len(rest) != 0, and add checks that ecdsaSig.R and ecdsaSig.S are non-nil (and
return an error if they are) before invoking ecdsa.Verify with ecdsaPubKey,
digestBytes and ecdsaSig.R/S; this ensures strict DER parsing and avoids nil
dereferences.
In `@pkg/signature/profiles/applicationprofile_adapter.go`:
- Around line 18-23: GetAnnotations on ApplicationProfileAdapter returns a newly
created map when profile.Annotations is nil but does not persist it, so callers'
mutations are lost; change GetAnnotations to allocate a new map when
a.profile.Annotations == nil, assign it back to a.profile.Annotations (so the
profile holds the map) and then return it, referencing the GetAnnotations
method, the ApplicationProfileAdapter receiver a, the profile.Annotations field,
and the existing SetAnnotations behavior if needed for consistency.
In `@pkg/signature/profiles/seccompprofile_adapter.go`:
- Around line 18-23: GetAnnotations currently returns a newly allocated map when
s.profile.Annotations is nil but does not attach it to the adapter, so mutations
are lost; update SeccompProfileAdapter.GetAnnotations to initialize
s.profile.Annotations in-place (set s.profile.Annotations =
make(map[string]string) when nil) and then return s.profile.Annotations so
callers can safely mutate the returned map directly.
In `@pkg/signature/sign_test.go`:
- Around line 196-199: The test setup calls SignProfileKeyless(tt.profile) but
ignores its returned error; update the setup branch where tt.setupSign is true
to capture and assert the error so failures surface immediately (e.g., err :=
SignProfileKeyless(tt.profile) and fail the test on non-nil error using the test
helper in scope such as t.Fatalf/t.Fatalf-like assertion or require.NoError).
Ensure the alternative path with tt.setupAnnotations remains unchanged and use
the same t instance for error reporting.
In `@pkg/signature/sign.go`:
- Around line 11-35: SignProfile currently dereferences the incoming profile
(calls profile.GetContent()) without validating it, which panics on a nil input;
update SignProfile to validate that the profile argument is not nil at the start
and return a clear error (e.g., fmt.Errorf("nil profile")) instead of
proceeding, and also ensure any subsequent use of profile (GetContent) is only
after that nil-check; reference the SignProfile function and the
SignableProfile/GetContent call when making the change.
In `@pkg/signature/verifier.go`:
- Around line 18-24: The Verify and VerifyAllowUntrusted methods on
CosignVerifier dereference v.adapter without nil-checking; update both methods
(Verify and VerifyAllowUntrusted) to first verify that v and v.adapter are
non-nil and return a clear error (e.g., "uninitialized CosignVerifier adapter"
or similar) if they are nil, otherwise call v.adapter.VerifyData(data, sig,
...); reference CosignVerifier, Verify, VerifyAllowUntrusted, and
adapter.VerifyData when applying the change.
In `@pkg/signature/verify.go`:
- Around line 11-23: The VerifyProfile entrypoint assumes the incoming profile
is non-nil and can panic when calling profile.GetAnnotations(); add a nil guard
at the start of VerifyProfile to validate the profile argument (e.g., if profile
== nil) and return a clear error before proceeding, so subsequent calls like
profile.GetAnnotations() are safe; update any tests or callers if needed to
handle the new error path.
---
Duplicate comments:
In `@cmd/sign-profile/main.go`:
- Around line 48-53: The bug is that when command is inferred (switch case
"sign",""), parseSignFlags() is called while os.Args still contains the original
argv, then later code unshifts "sign-profile" (argsRewritten path) causing the
first real flag to be dropped because parsing uses os.Args[2:]; fix by ensuring
the argv mutation happens before parsing or by parsing the correct slice based
on argsRewritten: move the os.Args = append([]string{"sign-profile"},
os.Args[1:]...) line to occur before calling parseSignFlags(), or change
parseSignFlags() invocation to pass the correct args (use os.Args[1:] when
argsRewritten is false and os.Args[2:] when a command token is present) so that
parseSignFlags() sees the first flag whether command was explicit or inferred;
update references to parseSignFlags, argsRewritten, and the switch handling
accordingly.
In `@docs/signing/README.md`:
- Around line 450-459: The ordered list numbering in the "Best Practices"
section of docs/signing/README.md is inconsistent—after the first "3." the
subsequent list item "3. **Version Your Profiles**" should be renumbered to
continue the sequence; update the numeric markers for "Version Your Profiles",
"Key Management for Local Signing", and "Audit Trail" so they follow the
previous item (e.g., 4., 5., 6.) to restore correct ordered-list continuity.
In `@pkg/signature/cosign_adapter.go`:
- Around line 202-227: The current strict path parses sig.Certificate but only
runs local sanity checks (IsCA, CommonName, dates) and later trusts
sig.Issuer/sig.Identity if non-empty; fix by performing a full x509 chain
verification against a configured set of trust anchors and intermediates (use
x509.Verify with proper Roots and Intermediates and time check) inside the block
that handles sig.Certificate when allowUntrusted is false, and require that
x509.Verify succeeds; additionally, do not accept sig.Issuer or sig.Identity as
authoritative unless they are derived from the verified certificate (e.g.,
compare sig.Issuer/sig.Identity to cert.Subject/subjectAltName or parsed OID
claims from cert) so only values bound to a certificate that successfully
verified are trusted.
---
Nitpick comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 252-258: Multiple code paths repeat creating an
objectcache.ProfileState with Completion:"failed", Status:"verification-failed",
Name:profile.Name and Error:err and then calling
apc.workloadIDToProfileState.Set(workloadID, profileState); factor this into a
single helper on the receiver (e.g. func (apc *ApplicationProfileCache)
setVerificationFailed(workloadID string, profile *ProfileType, err error)) that
constructs the ProfileState and calls apc.workloadIDToProfileState.Set; replace
the three duplicated blocks (the occurrences around the current diff and at
lines ~358-365 and ~589-596) with calls to that helper to keep status shaping
consistent.
In `@pkg/signature/cosign_adapter.go`:
- Around line 314-319: The code currently uses fmt.Sscanf to parse the
AnnotationTimestamp value (see AnnotationTimestamp handling and the local
variable ts), which allows partial numeric parsing; replace that with
strconv.ParseInt on the timestamp string (use base 10 and 64-bit) and assign the
parsed int64 to ts, returning a wrapped error if ParseInt fails to ensure strict
integer validation of the annotation.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
cmd/sign-profile/main.godocs/signing/README.mdgo.modpkg/config/config.gopkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/signature/annotations.gopkg/signature/cosign_adapter.gopkg/signature/interface.gopkg/signature/profiles/adapter_test.gopkg/signature/profiles/applicationprofile_adapter.gopkg/signature/profiles/seccompprofile_adapter.gopkg/signature/sign.gopkg/signature/sign_test.gopkg/signature/signer.gopkg/signature/verifier.gopkg/signature/verify.gopkg/signature/verify_test.go
|
@coderabbitai please close all addressed comments. |
|
✅ Actions performedComments resolved. Auto-approval is disabled; enable |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)
370-387:⚠️ Potential issue | 🔴 CriticalRe-verify the base profile before caching the merged result.
This path verifies
fullUserProfile, then refetchesoriginalProfilefrom storage and writes the merge back toworkloadIDToProfilewithout verifying that second fetch. With verification enabled, a tampered base profile can still bypass the new integrity check whenever a user-managed overlay is present.🛠️ Suggested fix
originalProfile, err := apc.storageClient.GetApplicationProfile(toMerge.profile.Namespace, toMerge.profile.Name) if err != nil { logger.L().Error("failed to get original profile", helpers.String("namespace", toMerge.profile.Namespace), helpers.String("profileName", toMerge.profile.Name), helpers.Error(err)) return } + if err := apc.verifyApplicationProfile(originalProfile, toMerge.wlid, "profile"); err != nil { + return + } // Merge the profiles mergedProfile := apc.performMerge(originalProfile, fullUserProfile)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around lines 370 - 387, The code verifies fullUserProfile but does not re-verify the freshly fetched base profile from storage, allowing a tampered base to bypass integrity checks; after calling apc.storageClient.GetApplicationProfile (the fetched originalProfile), call apc.verifyApplicationProfile(originalProfile, toMerge.wlid, "base profile") and handle any error the same way as the earlier verify (log and return) before calling apc.performMerge and writing the merged result into workloadIDToProfile so both overlay and base are validated.
♻️ Duplicate comments (2)
pkg/signature/sign_test.go (1)
200-204:⚠️ Potential issue | 🟡 MinorCheck signing setup error in test case.
The
SignObjectKeylesscall in the setup ignores the returned error. If signing fails during setup, the test will proceed with an unsigned profile, making downstream assertions misleading.💚 Proposed fix
t.Run(tt.name, func(t *testing.T) { if tt.setupSign { - SignObjectKeyless(tt.profile) + if err := SignObjectKeyless(tt.profile); err != nil { + t.Fatalf("SignObjectKeyless setup failed: %v", err) + } } else if tt.setupAnnotations != nil { tt.setupAnnotations(tt.profile) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/sign_test.go` around lines 200 - 204, The test setup is ignoring the error returned by SignObjectKeyless, which can leave the profile unsigned and cause misleading failures; update the setup branch where SignObjectKeyless(tt.profile) is called to capture its error and fail the test on error (e.g., use t.Fatalf or require.NoError) so that SignObjectKeyless and any errors are asserted, similar to how tt.setupAnnotations is invoked for setupAnnotations(tt.profile).pkg/signature/cosign_adapter.go (1)
287-370:⚠️ Potential issue | 🟠 MajorVerification relies on self-signed certificates without external trust anchors.
This continues the concern from previous reviews: the verification accepts certificates and public keys from the signature annotations without validating against Fulcio roots or Rekor transparency logs. An attacker who can modify the profile can also replace the signature and certificate with their own self-signed material.
The
allowUntrustedflag provides some distinction, but even "strict" mode (allowUntrusted=false) only validates:
- Certificate is not CA (line 305)
- Certificate is within validity period (lines 309-311)
- Identity matches CN (lines 313-315)
It does not validate the certificate chain against Fulcio roots or verify Rekor bundle entries.
For production use with real security requirements, consider integrating with sigstore's verification infrastructure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 287 - 370, The VerifyData function accepts certificates/public keys from the signature payload and only checks basic properties (IsCA, validity window, CN) controlled by allowUntrusted, which leaves verification vulnerable to self-signed or attacker-provided certs; update VerifyData to perform real chain and transparency verification: when allowUntrusted is false and c.useKeyless (or when a certificate is present) validate the certificate chain against Fulcio root(s) and, if present, verify the Rekor bundle/entry (or fail if missing) using sigstore/cosign utilities (e.g., call sigstore trust root verification and cosign.VerifyBundle or equivalent), ensure you reference sig.Certificate, sig.RekorBundle (or add it to Signature), and the VerifyData code paths that call sigstore_signature.LoadVerifier so the verifier is only created after successful chain + Rekor verification.
🧹 Nitpick comments (6)
pkg/signature/profiles/seccompprofile_adapter.go (1)
40-59: Consider initializing nil Labels for consistent JSON serialization.When
s.profile.Labelsis nil, the JSON output will include"labels": null, but when it's an empty map, it will be"labels": {}. This inconsistency between cluster-side and signing-side representations could cause hash mismatches during verification.The
ApplicationProfileAdapterhas normalization logic forPolicyByRuleIdmaps; consider applying similar treatment here forLabelsif consistency is required.♻️ Optional fix for label consistency
func (s *SeccompProfileAdapter) GetContent() interface{} { apiVersion := s.profile.APIVersion if apiVersion == "" { apiVersion = "spdx.softwarecomposition.kubescape.io/v1beta1" } kind := s.profile.Kind if kind == "" { kind = "SeccompProfile" } + labels := s.profile.Labels + if labels == nil { + labels = make(map[string]string) + } return map[string]interface{}{ "apiVersion": apiVersion, "kind": kind, "metadata": map[string]interface{}{ "name": s.profile.Name, "namespace": s.profile.Namespace, - "labels": s.profile.Labels, + "labels": labels, }, "spec": s.profile.Spec, } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/profiles/seccompprofile_adapter.go` around lines 40 - 59, The GetContent method in SeccompProfileAdapter returns s.profile.Labels as-is which yields "labels": null when nil and causes inconsistent JSON/hashing; update SeccompProfileAdapter.GetContent to normalize labels by replacing a nil s.profile.Labels with an empty map before building the returned map (i.e., ensure labels is a non-nil map[string]string when constructing the "metadata" entry) so the JSON always serializes to "{}" for empty labels.pkg/signature/cosign_adapter.go (3)
34-43: Prefer blank identifier imports over unused variable declarations.These variables are declared but never used, serving only to ensure the packages are linked. This is unconventional and may trigger linter warnings. Use blank identifier imports instead.
♻️ Proposed refactor
-var _ = cosign.Signature -var _ = providers.Enabled -var _ = bundle.RekorBundle{} -var _ = api.CertificateRequest{} -var _ = client.Rekor{} -var _ = models.LogEntry{} -var _ = fulcioroots.Get -var _ = oauthflow.OIDConnect -var _ = oauthflow.DefaultIDTokenGetterIf these imports are needed for side effects, use blank identifier imports in the import block:
import ( _ "github.com/sigstore/cosign/v2/pkg/cosign" // ... etc )If they're placeholders for future use, consider removing them until needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 34 - 43, Replace the unused variable declarations (cosign.Signature, providers.Enabled, bundle.RekorBundle, api.CertificateRequest, client.Rekor, models.LogEntry, fulcioroots.Get, oauthflow.OIDConnect, oauthflow.DefaultIDTokenGetter) with proper blank identifier imports in the package import block (e.g., import _ "module/path") for any packages required only for side effects, or remove these placeholder declarations entirely if the packages are not needed; update the import list to include the corresponding packages with blank identifier imports instead of keeping the var _ = ... lines so linters no longer flag unused variables.
217-246: Self-signed certificate has a one-year validity period.The generated certificate is valid for 365 days (line 229). For keyless signing, certificates are typically short-lived (minutes) to reduce the window of key compromise. For key-based signing intended for offline/air-gapped environments, a longer validity may be acceptable.
Ensure this aligns with your security model and consider making the validity period configurable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 217 - 246, The generateCertificate function currently sets a fixed 365-day validity; make the certificate lifetime configurable to support short-lived (minutes) keyless certs or longer offline certs: add a CertValidity (time.Duration) field or configuration on the CosignAdapter and use it when computing NotAfter (replace time.Now().Add(365*24*time.Hour) with time.Now().Add(c.CertValidity)), provide a sensible default (e.g., 15m for keyless) when constructing CosignAdapter, and ensure any callers or tests are updated to set/expect the configured duration.
416-436: Silent fallback from base64 to raw bytes may mask encoding issues.When base64 decoding fails, the code silently falls back to treating the value as raw bytes (lines 420-421, 427-428, 435-436). This could mask legitimate encoding errors or allow malformed data to pass through.
Consider logging a warning when falling back, or failing explicitly if the annotation is expected to be base64-encoded.
♻️ Proposed fix to add logging on fallback
sig.Signature, err = base64.StdEncoding.DecodeString(signatureB64) if err != nil { - // Try raw if base64 fails + // Try raw if base64 fails - this handles legacy or non-standard encodings sig.Signature = []byte(signatureB64) + // Consider: logger.L().Debug("Signature annotation not base64 encoded, using raw bytes") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 416 - 436, The code silently falls back to raw bytes when base64 decoding fails for sig.Signature, sig.Certificate and sig.RekorBundle (base64.StdEncoding.DecodeString used against annotations[AnnotationCertificate] and annotations[AnnotationRekorBundle]), which can hide encoding problems; update each decode branch to emit a warning (e.g., logger.Warnf or similar) when DecodeString returns an error that includes the annotation key and the decode error, and only fallback to raw bytes if configured to be tolerant (or alternatively return the error to the caller for strict mode); ensure the warning references sig.Signature, sig.Certificate and sig.RekorBundle so it’s clear which field had the issue.pkg/signature/profiles/applicationprofile_adapter.go (2)
67-76: Consider initializing nil Labels for consistent JSON serialization.Same concern as
SeccompProfileAdapter: whena.profile.Labelsis nil, JSON renders"labels": nullvs"labels": {}for empty maps, potentially causing hash mismatches during verification.♻️ Optional fix for label consistency
+ labels := a.profile.Labels + if labels == nil { + labels = make(map[string]string) + } return map[string]interface{}{ "apiVersion": apiVersion, "kind": kind, "metadata": map[string]interface{}{ "name": a.profile.Name, "namespace": a.profile.Namespace, - "labels": a.profile.Labels, + "labels": labels, }, "spec": a.profile.Spec, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/profiles/applicationprofile_adapter.go` around lines 67 - 76, The returned manifest may emit "labels": null when a.profile.Labels is nil, causing inconsistent JSON/hashing; update the ApplicationProfileAdapter code that builds the map (the block returning apiVersion/kind/metadata/spec) to ensure labels is always a non-nil map by replacing "labels": a.profile.Labels with a guarded value that returns an empty map (e.g., map[string]string{}) when a.profile.Labels == nil, otherwise use a.profile.Labels so serialization yields {} instead of null.
43-57: Normalization mutates the original profile object.The
PolicyByRuleIdnormalization directly modifiesa.profile.Spec.Containers[i].PolicyByRuleId. While this ensures consistent JSON representation for signing, it has the side effect of mutating the caller's profile object. If the caller expects the profile to remain unchanged after callingGetContent(), this could lead to unexpected behavior.Consider whether this mutation is intentional or if a defensive copy should be made.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/profiles/applicationprofile_adapter.go` around lines 43 - 57, The normalization loop in GetContent (which sets PolicyByRuleId on a.profile.Spec.Containers / InitContainers / EphemeralContainers) mutates the original profile; instead make a defensive deep copy of the profile/spec before normalizing so callers' objects are unchanged. Update the code in applicationprofile_adapter.go to copy a.profile (or at least a.profile.Spec and its Containers/InitContainers/EphemeralContainers and their PolicyByRuleId maps) into a local variable, perform the nil->make map initialization on that copy, and then use the copy for JSON/signing output (avoid changing a.profile in place). Ensure copies include container slices and nested maps so no original reference is modified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/sign-object/main.go`:
- Around line 43-47: The code currently rewrites any leading-dash command into
"sign" (using variables command and argsRewritten), which causes "sign-object
-h" or "--help" to show sign subcommand help; adjust the conditional so it first
checks for explicit global help flags (e.g., if command == "-h" || command ==
"--help" || (len(os.Args)>1 && os.Args[1] == "-h") etc.) and returns/prints
top-level usage instead of rewriting, otherwise fall back to the existing if
strings.HasPrefix(command, "-") { command = "sign"; argsRewritten = true }
logic; update the handling around argsRewritten and any help-display code paths
to ensure global help is preserved.
In `@docs/signing/README.md`:
- Around line 255-260: Update the README entry for the --output and
--public-only flags to accurately describe the file layout: clarify that the CLI
writes the private key to the path given by --output and writes the public key
to the same path with a ".pub" suffix (unless --public-only is set, in which
case only the public file is written), and remove or replace the current
phrasing that implies both keys are written into a single PEM file; specifically
edit the table descriptions for `--output` and `--public-only` to reflect this
behavior so users know which file to use for signing.
- Around line 70-102: The Mermaid diagram block in README.md is plain text and
should be fenced so it renders and passes markdownlint; wrap the entire graph
(from the "Diagram" header through the closing triple backticks) in a fenced
code block starting with ```mermaid and ending with ``` so the graph (graph TB
... end) is treated as a mermaid diagram (look for the block containing "Signing
Flow", "Verification Flow", and the mermaid graph statements like A[K8s
Resource] --> B[Adapter]) and replace the current plain text with the fenced
mermaid block.
In `@go.mod`:
- Line 71: Update the modernc.org/sqlite dependency in go.mod from v1.40.1 to
v1.46.1 and refresh module files: change the version string for
modernc.org/sqlite to v1.46.1, then run the Go tooling (e.g., go get
modernc.org/sqlite@v1.46.1 and go mod tidy) to update go.sum and ensure builds
pass; verify imports and any API changes in code referencing modernc.org/sqlite
compile cleanly.
- Around line 49-52: The go.mod lists outdated sigstore modules; update the
module versions for github.com/sigstore/cosign/v2 -> v3.0.5,
github.com/sigstore/fulcio -> v1.8.5, and github.com/sigstore/rekor -> v1.5.1,
then run go mod tidy and the test suite to catch any breakage; note that
upgrading github.com/sigstore/cosign/v2 to v3.0.5 is a major-version bump that
may require API changes—search usages of the cosign package imports/usages to
adjust call sites accordingly and fix any compile/test failures.
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 592-595: The code currently bails out of addContainer when
verifyApplicationProfile(fullProfile, workloadID, "user-defined profile")
returns an error, which prevents the later containerIDToInfo.Set and thus
updateAllProfiles from seeing the container; change this to handle a failed
optional/user-defined profile by logging the verification error (or recording it
on the container entry) and continuing execution so that containerIDToInfo.Set
is still called; keep the existing behavior for required profile verification
but do not return from addContainer on user-defined profile verify
failures—ensure verifyApplicationProfile is only fatal for required profiles and
non-fatal for the "user-defined profile" case.
In `@pkg/rulemanager/ruleswatcher/watcher.go`:
- Around line 149-152: The verifyRules method reads
w.cfg.EnableSignatureVerification without guarding for a nil cfg; update
verifyRules to treat a nil w.cfg as verification disabled (i.e., return nil if
w.cfg == nil || !w.cfg.EnableSignatureVerification) or alternatively normalize
w.cfg in the constructor (NewRulesWatcher) to a default config with
EnableSignatureVerification=false so verifyRules can safely access the flag;
reference the verifyRules method and NewRulesWatcher to locate where to
implement the guard/normalization.
- Around line 99-105: The verifyRules(rules) call already logs verification
failures, so remove the duplicate logger.L().Warning(...) block inside the
RulesWatcher loop: when verifyRules returns an error, simply increment
skippedVerificationCount and continue without emitting another warning; keep the
verifyRules function as the sole logger for signature failures and ensure only
skippedVerificationCount++ and continue remain in the error branch where
verifyRules is invoked.
- Around line 155-156: Replace the brittle string comparison against err.Error()
in the watcher (the isMissingSig check) with an errors.Is() check against a
sentinel error exported from pkg/signature (e.g., signature.ErrObjectNotSigned);
update pkg/signature to export that sentinel (or a helper like
signature.IsObjectNotSigned(err)) and have VerifyObject return or wrap that
sentinel when the annotation is missing, then in watcher.go use errors.Is(err,
signature.ErrObjectNotSigned) (or call signature.IsObjectNotSigned) to classify
unsigned objects instead of fmt.Sprintf string matching.
In `@pkg/signature/cluster_flow_test.go`:
- Around line 86-98: The test currently ignores the certificate path because
generateTestCertificate does not use privKey and the test calls
verifier.VerifySignature using privKey.PublicKey directly; update the test to
derive a real x509 certificate from privKey (or have generateTestCertificate
accept and use privKey) and store its base64 in the
"signature.kubescape.io/certificate" annotation, then decode via
cosignAdapter.DecodeSignatureFromAnnotations and either construct the verifier
from the parsed certificate (not privKey.PublicKey) or exercise the higher-level
flow by calling VerifyObject or CosignVerifier so the code path that
extracts/parses/verifies the certificate from annotations is exercised; ensure
references to certBytes, sig, and the annotated values are used for verification
rather than privKey.PublicKey.
- Around line 82-104: The test currently ignores errors and only logs
verification results; update it to fail immediately on any error and assert
verification succeeds: check and handle returned errors from ecdsa.GenerateKey,
sigstore_signature.LoadECDSASigner, signer.SignMessage, generateTestCertificate
(if it can error), cosignAdapter.DecodeSignatureFromAnnotations,
sigstore_signature.LoadECDSAVerifier and hex.DecodeString, and replace t.Logf
calls around verifier.VerifySignature with assertions (e.g.,
t.Fatalf/t.Fatalf-style failure) that the VerifySignature calls on verifier (for
both bytes.NewReader([]byte(hash)) and bytes.NewReader(hashBytes)) return nil
(success); ensure each step uses the specific symbols shown (GenerateKey,
LoadECDSASigner, SignMessage, DecodeSignatureFromAnnotations, LoadECDSAVerifier,
VerifySignature) so failures stop the test immediately.
In `@pkg/signature/profiles/rules_adapter.go`:
- Around line 17-21: The GetAnnotations method currently mutates r.rules by
allocating r.rules.Annotations when nil; remove that mutation and simply return
the annotations as-is (i.e., return r.rules.Annotations without creating a new
map) so read-only callers (e.g., VerifyObject) do not change unsigned Rules;
ensure any code that needs to write or persist annotations initializes the map
in the write path (where mutations are expected) instead of here.
---
Outside diff comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 370-387: The code verifies fullUserProfile but does not re-verify
the freshly fetched base profile from storage, allowing a tampered base to
bypass integrity checks; after calling apc.storageClient.GetApplicationProfile
(the fetched originalProfile), call
apc.verifyApplicationProfile(originalProfile, toMerge.wlid, "base profile") and
handle any error the same way as the earlier verify (log and return) before
calling apc.performMerge and writing the merged result into workloadIDToProfile
so both overlay and base are validated.
---
Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 287-370: The VerifyData function accepts certificates/public keys
from the signature payload and only checks basic properties (IsCA, validity
window, CN) controlled by allowUntrusted, which leaves verification vulnerable
to self-signed or attacker-provided certs; update VerifyData to perform real
chain and transparency verification: when allowUntrusted is false and
c.useKeyless (or when a certificate is present) validate the certificate chain
against Fulcio root(s) and, if present, verify the Rekor bundle/entry (or fail
if missing) using sigstore/cosign utilities (e.g., call sigstore trust root
verification and cosign.VerifyBundle or equivalent), ensure you reference
sig.Certificate, sig.RekorBundle (or add it to Signature), and the VerifyData
code paths that call sigstore_signature.LoadVerifier so the verifier is only
created after successful chain + Rekor verification.
In `@pkg/signature/sign_test.go`:
- Around line 200-204: The test setup is ignoring the error returned by
SignObjectKeyless, which can leave the profile unsigned and cause misleading
failures; update the setup branch where SignObjectKeyless(tt.profile) is called
to capture its error and fail the test on error (e.g., use t.Fatalf or
require.NoError) so that SignObjectKeyless and any errors are asserted, similar
to how tt.setupAnnotations is invoked for setupAnnotations(tt.profile).
---
Nitpick comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 34-43: Replace the unused variable declarations (cosign.Signature,
providers.Enabled, bundle.RekorBundle, api.CertificateRequest, client.Rekor,
models.LogEntry, fulcioroots.Get, oauthflow.OIDConnect,
oauthflow.DefaultIDTokenGetter) with proper blank identifier imports in the
package import block (e.g., import _ "module/path") for any packages required
only for side effects, or remove these placeholder declarations entirely if the
packages are not needed; update the import list to include the corresponding
packages with blank identifier imports instead of keeping the var _ = ... lines
so linters no longer flag unused variables.
- Around line 217-246: The generateCertificate function currently sets a fixed
365-day validity; make the certificate lifetime configurable to support
short-lived (minutes) keyless certs or longer offline certs: add a CertValidity
(time.Duration) field or configuration on the CosignAdapter and use it when
computing NotAfter (replace time.Now().Add(365*24*time.Hour) with
time.Now().Add(c.CertValidity)), provide a sensible default (e.g., 15m for
keyless) when constructing CosignAdapter, and ensure any callers or tests are
updated to set/expect the configured duration.
- Around line 416-436: The code silently falls back to raw bytes when base64
decoding fails for sig.Signature, sig.Certificate and sig.RekorBundle
(base64.StdEncoding.DecodeString used against annotations[AnnotationCertificate]
and annotations[AnnotationRekorBundle]), which can hide encoding problems;
update each decode branch to emit a warning (e.g., logger.Warnf or similar) when
DecodeString returns an error that includes the annotation key and the decode
error, and only fallback to raw bytes if configured to be tolerant (or
alternatively return the error to the caller for strict mode); ensure the
warning references sig.Signature, sig.Certificate and sig.RekorBundle so it’s
clear which field had the issue.
In `@pkg/signature/profiles/applicationprofile_adapter.go`:
- Around line 67-76: The returned manifest may emit "labels": null when
a.profile.Labels is nil, causing inconsistent JSON/hashing; update the
ApplicationProfileAdapter code that builds the map (the block returning
apiVersion/kind/metadata/spec) to ensure labels is always a non-nil map by
replacing "labels": a.profile.Labels with a guarded value that returns an empty
map (e.g., map[string]string{}) when a.profile.Labels == nil, otherwise use
a.profile.Labels so serialization yields {} instead of null.
- Around line 43-57: The normalization loop in GetContent (which sets
PolicyByRuleId on a.profile.Spec.Containers / InitContainers /
EphemeralContainers) mutates the original profile; instead make a defensive deep
copy of the profile/spec before normalizing so callers' objects are unchanged.
Update the code in applicationprofile_adapter.go to copy a.profile (or at least
a.profile.Spec and its Containers/InitContainers/EphemeralContainers and their
PolicyByRuleId maps) into a local variable, perform the nil->make map
initialization on that copy, and then use the copy for JSON/signing output
(avoid changing a.profile in place). Ensure copies include container slices and
nested maps so no original reference is modified.
In `@pkg/signature/profiles/seccompprofile_adapter.go`:
- Around line 40-59: The GetContent method in SeccompProfileAdapter returns
s.profile.Labels as-is which yields "labels": null when nil and causes
inconsistent JSON/hashing; update SeccompProfileAdapter.GetContent to normalize
labels by replacing a nil s.profile.Labels with an empty map before building the
returned map (i.e., ensure labels is a non-nil map[string]string when
constructing the "metadata" entry) so the JSON always serializes to "{}" for
empty labels.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e772b00d-4a8d-464d-8b32-21513c9e0a1c
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (25)
cmd/main.gocmd/sign-object/main.godocs/signing/README.mdgo.modpkg/config/config.gopkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/rulemanager/ruleswatcher/watcher.gopkg/signature/annotations.gopkg/signature/cluster_flow_test.gopkg/signature/cluster_scenario_test.gopkg/signature/cosign_adapter.gopkg/signature/cosign_adapter_test.gopkg/signature/interface.gopkg/signature/profiles/adapter_test.gopkg/signature/profiles/applicationprofile_adapter.gopkg/signature/profiles/empty_typemeta_test.gopkg/signature/profiles/rules_adapter.gopkg/signature/profiles/rules_adapter_test.gopkg/signature/profiles/seccompprofile_adapter.gopkg/signature/sign.gopkg/signature/sign_test.gopkg/signature/signer.gopkg/signature/verifier.gopkg/signature/verify.gopkg/signature/verify_test.go
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
cmd/sign-object/main.go (1)
43-47:⚠️ Potential issue | 🟡 MinorGlobal help flags still get rewritten to
sign.Running
sign-object -horsign-object --helpmatches thestrings.HasPrefix(command, "-")check before reaching the explicit help cases in the switch, causing users to see sign-specific help instead of global usage.🐛 Proposed fix
command = os.Args[1] argsRewritten := false + if command == "-h" || command == "--help" { + printUsage() + os.Exit(0) + } if strings.HasPrefix(command, "-") { command = "sign" argsRewritten = true },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/sign-object/main.go` around lines 43 - 47, The current rewrite logic sets command = "sign" when strings.HasPrefix(command, "-"), which incorrectly catches global help flags like "-h" and "--help"; change the condition so help flags are exempt (e.g., check for "-h" and "--help" first or require strings.HasPrefix(command, "-") && command != "-h" && command != "--help") so argsRewritten is only set for real subcommand shortcuts while preserving the explicit help cases in the existing switch that handle "-h"/"--help".docs/signing/README.md (2)
72-104:⚠️ Potential issue | 🟡 MinorAdd the missing opening
mermaidfence.This block still starts as plain text and only has the closing fence, which matches the markdownlint failures on Line 89 and Line 104.
Suggested fix
### Diagram +```mermaid graph TB subgraph "Signing Flow" A[K8s Resource] --> B[Adapter] @@ L --> M</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/signing/README.mdaround lines 72 - 104, Add the missing opening fenced
code block marker for the Mermaid diagram by inserting an opening "mermaid" fence immediately before the "graph TB" line so the block is recognized as Mermaid (the block already contains the closing ""); ensure there is no extra
indentation and the opening fence matches the existing closing fence to resolve
the markdownlint errors around the diagram section.</details> --- `257-263`: _⚠️ Potential issue_ | _🟡 Minor_ **Fix the `generate-keypair` output description.** The table still implies both keys are written into one PEM file. The CLI writes the private key to `--output` and the public key to `--output + ".pub"`; `--public-only` should say that only the public file is emitted. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@docs/signing/README.md` around lines 257 - 263, Update the generate-keypair flags documentation to accurately describe where each key is written: change the `--output` description to state it writes the private key to the specified PEM file and that the public key is written to `--output + ".pub"`, and change the `--public-only` description to state it emits only the public key file (i.e. only `--output + ".pub"`) rather than implying a combined PEM; reference the `--output` and `--public-only` flags in the README table for these exact wording changes. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🧹 Nitpick comments (2)</summary><blockquote> <details> <summary>cmd/sign-object/main.go (2)</summary><blockquote> `52-63`: **Remove dead code: `os.Args` modifications after parsing.** These `os.Args` assignments occur after `parse*Flags()` functions have already completed parsing. They have no effect on the program behavior. <details> <summary>♻️ Proposed cleanup</summary> ```diff case "sign", "": parseSignFlags() - if argsRewritten { - os.Args = append([]string{"sign-object"}, os.Args[1:]...) - } case "verify": parseVerifyFlags() - os.Args = append([]string{"sign-object verify"}, os.Args[2:]...) case "generate-keypair": parseGenerateFlags() - os.Args = append([]string{"sign-object generate-keypair"}, os.Args[2:]...) case "extract-signature": parseExtractFlags() - os.Args = append([]string{"sign-object extract-signature"}, os.Args[2:]...) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@cmd/sign-object/main.go` around lines 52 - 63, Remove the dead os.Args reassignments that occur after flag parsing: the append calls modifying os.Args in the branches for "verify", "generate-keypair", and "extract-signature" (and the argsRewritten handling) have no effect because parseVerifyFlags(), parseGenerateFlags(), and parseExtractFlags() already completed parsing; delete those os.Args = append(...) lines (and any unused argsRewritten-related branch if it is now unused) to clean up dead code while keeping the parse*Flags() calls intact. ``` </details> --- `387-391`: **Certificate type display may silently fail for non-PEM certificates.** The signature annotations store certificates as base64-encoded raw bytes. `pem.Decode` returns `nil` for raw DER-encoded certificates, silently skipping the certificate type display. Consider parsing as DER if PEM decode fails. <details> <summary>♻️ Suggested improvement</summary> ```diff block, _ := pem.Decode(sig.Certificate) if block != nil { fmt.Printf(" Certificate Type: %s\n", block.Type) + } else if len(sig.Certificate) > 0 { + // Try parsing as DER + cert, err := x509.ParseCertificate(sig.Certificate) + if err == nil { + fmt.Printf(" Certificate Subject: %s\n", cert.Subject.CommonName) + } } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@cmd/sign-object/main.go` around lines 387 - 391, The code currently uses pem.Decode(sig.Certificate) and only prints block.Type when block != nil, which silently skips DER/raw certificates; update the logic in the certificate handling (around pem.Decode and printing of certificate type) to attempt DER parsing when pem.Decode returns nil: first try pem.Decode(sig.Certificate) and print block.Type if present, otherwise try parsing sig.Certificate as a DER x509 certificate (using x509.ParseCertificate) and on success print an appropriate type string (e.g., "X.509 DER") or the certificate's Subject/Issuer; ensure error handling/logging for failed parses so non-PEM certificates don't silently skip type output. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go:
- Around line 327-340: The current code verifies the post-merge object
(mergedNN) using signature.VerifyObjectStrict after performMerge, but
performMerge mutates content while retaining original annotations so
verification will incorrectly fail for valid user overrides; instead, when
nnc.cfg.EnableSignatureVerification is true, call signature.VerifyObjectStrict
on the fetched source objects (originalNN and fullUserNN) before performMerge
(using profiles.NewNetworkNeighborhoodAdapter on each) OR, if mergedNN must be
cacheable, re-sign the merged result after performMerge (update mergedNN
annotations and call the signing API) and then use VerifyObjectStrict on the
re-signed mergedNN; update the code paths around performMerge,
NewNetworkNeighborhoodAdapter, and VerifyObjectStrict accordingly so signature
checks validate the unmodified source or a freshly-signed merged object.- Around line 243-254: The signature-verification branch currently logs and
continues without updating workloadIDToProfileState, causing callers of
GetNetworkNeighborhoodState to miss the failure; modify the block inside
nnc.cfg.EnableSignatureVerification (where adapter :=
profiles.NewNetworkNeighborhoodAdapter(fullNN) and
signature.VerifyObjectStrict(adapter) is called) to persist the verification
failure into workloadIDToProfileState: create or update the ProfileState entry
for workloadID (and namespace/name as appropriate) to mark it failed/errored and
include the verification error (err.Error()) and any relevant metadata before
continuing, so the state reflects the rejected profile.
Duplicate comments:
In@cmd/sign-object/main.go:
- Around line 43-47: The current rewrite logic sets command = "sign" when
strings.HasPrefix(command, "-"), which incorrectly catches global help flags
like "-h" and "--help"; change the condition so help flags are exempt (e.g.,
check for "-h" and "--help" first or require strings.HasPrefix(command, "-") &&
command != "-h" && command != "--help") so argsRewritten is only set for real
subcommand shortcuts while preserving the explicit help cases in the existing
switch that handle "-h"/"--help".In
@docs/signing/README.md:
- Around line 72-104: Add the missing opening fenced code block marker for the
Mermaid diagram by inserting an opening "mermaid" fence immediately before the "graph TB" line so the block is recognized as Mermaid (the block already contains the closing ""); ensure there is no extra indentation and the
opening fence matches the existing closing fence to resolve the markdownlint
errors around the diagram section.- Around line 257-263: Update the generate-keypair flags documentation to
accurately describe where each key is written: change the--outputdescription
to state it writes the private key to the specified PEM file and that the public
key is written to--output + ".pub", and change the--public-only
description to state it emits only the public key file (i.e. only--output + ".pub") rather than implying a combined PEM; reference the--outputand
--public-onlyflags in the README table for these exact wording changes.
Nitpick comments:
In@cmd/sign-object/main.go:
- Around line 52-63: Remove the dead os.Args reassignments that occur after flag
parsing: the append calls modifying os.Args in the branches for "verify",
"generate-keypair", and "extract-signature" (and the argsRewritten handling)
have no effect because parseVerifyFlags(), parseGenerateFlags(), and
parseExtractFlags() already completed parsing; delete those os.Args =
append(...) lines (and any unused argsRewritten-related branch if it is now
unused) to clean up dead code while keeping the parse*Flags() calls intact.- Around line 387-391: The code currently uses pem.Decode(sig.Certificate) and
only prints block.Type when block != nil, which silently skips DER/raw
certificates; update the logic in the certificate handling (around pem.Decode
and printing of certificate type) to attempt DER parsing when pem.Decode returns
nil: first try pem.Decode(sig.Certificate) and print block.Type if present,
otherwise try parsing sig.Certificate as a DER x509 certificate (using
x509.ParseCertificate) and on success print an appropriate type string (e.g.,
"X.509 DER") or the certificate's Subject/Issuer; ensure error handling/logging
for failed parses so non-PEM certificates don't silently skip type output.</details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `2d6ebf82-5539-4d16-9058-e53243fbe4fb` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 411ed7b076e8d520b45ac3a6e8309898be137423 and 479285c088fb78f1e1af7ad599f91f87d4fc27c9. </details> <details> <summary>📒 Files selected for processing (3)</summary> * `cmd/sign-object/main.go` * `docs/signing/README.md` * `pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go
Outdated
Show resolved
Hide resolved
Implement generic signing and verification framework for ApplicationProfiles and SeccompProfiles using ECDSA P-256 signatures compatible with cosign. Key features: - SignableProfile interface for extensible profile type support - Keyless signing support using OIDC identity providers - Key-based signing for offline/air-gapped environments - Signature storage in profile metadata.annotations - CLI tool (sign-profile) for signing and verifying profiles - Automatic verification in ApplicationProfileCache on profile load Core implementation: - pkg/signature/ package with ECDSA P-256 signing/verification - Profile adapters for ApplicationProfile and SeccompProfile - EnableProfileVerification config flag (default: false) - On verification failure: skip profile with warning (don't crash) Files: - pkg/signature/annotations.go, interface.go, sign.go, verify.go - pkg/signature/cosign_adapter.go, signer.go, verifier.go - pkg/signature/profiles/applicationprofile_adapter.go - pkg/signature/profiles/seccompprofile_adapter.go - cmd/sign-profile/main.go - pkg/config/config.go - add EnableProfileVerification flag - pkg/objectcache/applicationprofilecache/ - integrate verification at load time - docs/signing/README.md - comprehensive documentation Tests: - 15 signature package tests covering signing, verification, tampering detection - 5 adapter tests for ApplicationProfile and SeccompProfile adapters - All tests passing Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com> Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com> Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com> Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
- Implement RulesAdapter implementing SignableProfile interface - Add signature verification in RulesWatcher when enabled - Pass config to RulesWatcher to enable profile verification toggle - Add comprehensive tests for rules signing and verification - Track skipped rules due to verification failures in logs - Maintain consistent behavior with application and seccomp profiles Co-authored-by: Cerebras Agent <193945191+isaact-cerebras@users.noreply.github.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go (1)
329-373:⚠️ Potential issue | 🟠 MajorDrop stale signature annotations before caching the merged profile.
performMergechanges the signed content, butmergedNNstill inherits the original signature metadata fromDeepCopy(). That leaves a mutated object in cache that still looks signed, and any downstream re-verification will fail against stale annotations.🧹 Proposed fix
// Merge the network neighborhoods mergedNN := nnc.performMerge(originalNN, fullUserNN) + for _, key := range []string{ + signature.AnnotationSignature, + signature.AnnotationCertificate, + signature.AnnotationRekorBundle, + signature.AnnotationIssuer, + signature.AnnotationIdentity, + signature.AnnotationTimestamp, + } { + delete(mergedNN.Annotations, key) + } // Update the cache with the merged network neighborhood nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go` around lines 329 - 373, The merged network neighborhood returned by nnc.performMerge(originalNN, fullUserNN) still carries signature-related annotations from the DeepCopy of original objects; before calling nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN) remove/clear the stale signature metadata (i.e., any annotation keys the signature.VerifyObjectStrict/Sign logic uses) from mergedNN.Annotations so the cached object no longer appears signed and won't trigger false re-verification failures.pkg/signature/cosign_adapter.go (1)
334-397:⚠️ Potential issue | 🔴 Critical
allowUntrusted=falsestill trusts key material from the object itself.The strict path accepts a raw
PUBLIC KEYPEM from annotations, and even theCERTIFICATEbranch never chains the leaf to a trusted Fulcio root or validates Rekor/CT material. A user who can rewrite the object can therefore replace both the content and verification key and still makeVerifyObjectStrictpass. Sigstore's keyless verification requires the certificate chain plus Rekor/CT evidence, not just an embedded PEM blob. (docs.sigstore.dev)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 334 - 397, The current strict verification path (in the pem.Decode branch handling "CERTIFICATE" and "PUBLIC KEY" inside VerifyObjectStrict) accepts an embedded PUBLIC KEY PEM and does not validate a certificate chain or Rekor/CT evidence, allowing object authors to substitute keys; fix by forbidding or treating embedded public keys as untrusted when allowUntrusted==false and by requiring and validating a proper x509 chain and SignedEntryTimestamp/Rekor proof for Fulcio-issued certificates: parse the certificate chain (not just the leaf) from sig.Certificate, verify it against the Fulcio root set and system roots, validate certificate validity period and key usage, and run Sigstore/Cosign Rekor/CT verification APIs (the library verification that checks SignedEntryTimestamp/Rekor proof) before calling sigstore_signature.LoadVerifier; ensure any fallback PUBLIC KEY handling is only allowed when allowUntrusted==true.
🧹 Nitpick comments (2)
pkg/signature/profiles/networkneighborhood_adapter_test.go (1)
11-65: Add coverage for the empty-TypeMetapath.This test only exercises the branch where
APIVersionandKindare already populated.GetContent()has separate defaulting logic for emptyTypeMeta, and those fields are part of the canonical hash used by signing/verification, so a regression there would not be caught here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/profiles/networkneighborhood_adapter_test.go` around lines 11 - 65, Add a variant in TestNetworkNeighborhoodAdapter that constructs a v1beta1.NetworkNeighborhood with an empty TypeMeta (no Kind/APIVersion), pass it through NewNetworkNeighborhoodAdapter and call adapter.GetContent(), then assert the returned content map contains the defaulted "kind" == "NetworkNeighborhood" and "apiVersion" == "softwarecomposition.kubescape.io/v1beta1" (and validate metadata.name/namespace and spec container content as in the existing assertions); this ensures GetContent()'s defaulting path for empty TypeMeta is covered and included in signing/verification hashing.pkg/signature/cluster_flow_test.go (1)
104-112: Exercise the package-level annotation flow in this test.The fixture hand-writes only
signatureandcertificateannotations and then verifies withcert.PublicKeydirectly, so it won't catch regressions inEncodeSignatureToAnnotations, issuer/identity/timestamp handling, orVerifyObject*. Building the annotations viaEncodeSignatureToAnnotationsand assertingVerifyObjectAllowUntrusted(adapter)here would cover the flow the rest of the package actually uses.Also applies to: 117-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cluster_flow_test.go` around lines 104 - 112, The test currently hand-sets "signature" and "certificate" annotations and decodes via cosignAdapter.DecodeSignatureFromAnnotations; instead exercise the package-level annotation flow by using cosignAdapter.EncodeSignatureToAnnotations (or EncodeSignatureToAnnotations) to produce the annotations from sig and certBytes, apply them to adapter via adapter.SetAnnotations, then call cosignAdapter.VerifyObjectAllowUntrusted(adapter) (or VerifyObjectAllowUntrusted) and assert no error; this ensures EncodeSignatureToAnnotations, issuer/identity/timestamp handling, and VerifyObject* are validated rather than bypassed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 279-318: The code currently flips the entire workload state to
"verification-failed" inside verifyApplicationProfile, which wrongly affects the
whole workload when a user-managed overlay is rejected; change
verifyApplicationProfile to accept a recordFailure bool (e.g.,
verifyApplicationProfile(profile, workloadID, context, recordFailure)) and only
call apc.setVerificationFailed(workloadID, profile.Name, err) when recordFailure
is true; update callers so the normal profile load path passes
recordFailure=true and handleUserManagedProfile (and the user-overlay path)
passes recordFailure=false, and ensure any other direct calls (the code around
the existing setVerificationFailed usage) are adjusted likewise.
- Around line 592-605: The verifyApplicationProfile(...) failure branch
currently logs the error but then continues and overwrites the profile state,
allowing an invalid user-defined profile to be loaded; instead, after calling
verifyApplicationProfile(fullProfile, workloadID, "user-defined profile") and
detecting an error, return/skip further processing for that userDefinedProfile
so it is not applied, and do not call apc.workloadIDToProfileState.Set(...) with
a new ProfileState that wipes fields set by setVerificationFailed(); preserve or
rely on setVerificationFailed() to record the failure (or update only its Error
field if necessary) and ensure subsequent code that applies the profile checks
the verification result before loading.
In `@pkg/signature/cosign_adapter.go`:
- Around line 129-136: The provider-enabled branch currently hard-codes identity
and issuer after calling providers.Provide; instead parse the returned OIDC
token (tok) the same way the interactive/fallback flow does (use the existing
token parsing routine or JWT/OIDC parser used elsewhere to produce oidcToken)
and set identity = oidcToken.Subject and issuer = oidcToken.Issuer (remove the
hard-coded sigstoreOIDC/sigstoreIssuer assignments). Ensure you reuse the same
parsing/validation path used by the non-CI flow so the subject/issuer come from
the token claims.
- Around line 348-350: The certificate validity check currently uses time.Now()
and should instead use the signature's signing time: replace
time.Now().Before(cert.NotBefore) / time.Now().After(cert.NotAfter) with checks
against time.Unix(sig.Timestamp, 0) (i.e., use sig.Timestamp from the Signature
struct) so the Fulcio certificate is validated at signing time; update the logic
around cert.NotBefore and cert.NotAfter in the function that receives the
Signature (sig) to compare against time.Unix(sig.Timestamp, 0) rather than
time.Now().
In `@pkg/signature/verify.go`:
- Around line 22-28: The code currently returns a generic fmt.Errorf when
obj.GetAnnotations() is nil which prevents callers from detecting unsigned
objects via errors.Is; change the nil-annotations branch in verify.go to return
the sentinel ErrObjectNotSigned instead (or wrap it with %w if extra context is
desired) so that the check for AnnotationSignature and callers using
errors.Is(err, ErrObjectNotSigned) behave consistently; update the block around
GetAnnotations(), AnnotationSignature and ErrObjectNotSigned accordingly.
---
Duplicate comments:
In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-373: The merged network neighborhood returned by
nnc.performMerge(originalNN, fullUserNN) still carries signature-related
annotations from the DeepCopy of original objects; before calling
nnc.workloadIDToNetworkNeighborhood.Set(toMerge.wlid, mergedNN) remove/clear the
stale signature metadata (i.e., any annotation keys the
signature.VerifyObjectStrict/Sign logic uses) from mergedNN.Annotations so the
cached object no longer appears signed and won't trigger false re-verification
failures.
In `@pkg/signature/cosign_adapter.go`:
- Around line 334-397: The current strict verification path (in the pem.Decode
branch handling "CERTIFICATE" and "PUBLIC KEY" inside VerifyObjectStrict)
accepts an embedded PUBLIC KEY PEM and does not validate a certificate chain or
Rekor/CT evidence, allowing object authors to substitute keys; fix by forbidding
or treating embedded public keys as untrusted when allowUntrusted==false and by
requiring and validating a proper x509 chain and SignedEntryTimestamp/Rekor
proof for Fulcio-issued certificates: parse the certificate chain (not just the
leaf) from sig.Certificate, verify it against the Fulcio root set and system
roots, validate certificate validity period and key usage, and run
Sigstore/Cosign Rekor/CT verification APIs (the library verification that checks
SignedEntryTimestamp/Rekor proof) before calling
sigstore_signature.LoadVerifier; ensure any fallback PUBLIC KEY handling is only
allowed when allowUntrusted==true.
---
Nitpick comments:
In `@pkg/signature/cluster_flow_test.go`:
- Around line 104-112: The test currently hand-sets "signature" and
"certificate" annotations and decodes via
cosignAdapter.DecodeSignatureFromAnnotations; instead exercise the package-level
annotation flow by using cosignAdapter.EncodeSignatureToAnnotations (or
EncodeSignatureToAnnotations) to produce the annotations from sig and certBytes,
apply them to adapter via adapter.SetAnnotations, then call
cosignAdapter.VerifyObjectAllowUntrusted(adapter) (or
VerifyObjectAllowUntrusted) and assert no error; this ensures
EncodeSignatureToAnnotations, issuer/identity/timestamp handling, and
VerifyObject* are validated rather than bypassed.
In `@pkg/signature/profiles/networkneighborhood_adapter_test.go`:
- Around line 11-65: Add a variant in TestNetworkNeighborhoodAdapter that
constructs a v1beta1.NetworkNeighborhood with an empty TypeMeta (no
Kind/APIVersion), pass it through NewNetworkNeighborhoodAdapter and call
adapter.GetContent(), then assert the returned content map contains the
defaulted "kind" == "NetworkNeighborhood" and "apiVersion" ==
"softwarecomposition.kubescape.io/v1beta1" (and validate metadata.name/namespace
and spec container content as in the existing assertions); this ensures
GetContent()'s defaulting path for empty TypeMeta is covered and included in
signing/verification hashing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 70d92705-634a-49d6-b279-4dc77f2bb1ce
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (13)
cmd/sign-object/main.godocs/signing/README.mdgo.modpkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.gopkg/rulemanager/ruleswatcher/watcher.gopkg/signature/annotations.gopkg/signature/cluster_flow_test.gopkg/signature/cosign_adapter.gopkg/signature/profiles/networkneighborhood_adapter.gopkg/signature/profiles/networkneighborhood_adapter_test.gopkg/signature/profiles/rules_adapter.gopkg/signature/verify.go
🚧 Files skipped from review as they are similar to previous changes (2)
- pkg/signature/annotations.go
- docs/signing/README.md
pkg/objectcache/applicationprofilecache/applicationprofilecache.go
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/objectcache/applicationprofilecache/applicationprofilecache.go (1)
594-617:⚠️ Potential issue | 🔴 CriticalUser-defined profile is loaded into cache even when verification fails.
When signature verification fails for a user-defined profile (lines 596-608), the code logs a warning and updates the profile state with an error, but then falls through to load the profile into the cache at line 611. This contradicts the stated requirement to skip profiles on verification failure and could allow untrusted profiles to be used.
🐛 Proposed fix
// Verify signature if enabled if err := apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile"); err != nil { logger.L().Warning("user-defined profile signature verification failed, continuing without signature", helpers.String("containerID", containerID), helpers.String("workloadID", workloadID), helpers.String("namespace", container.K8s.Namespace), helpers.String("profileName", userDefinedProfile), helpers.Error(err)) - // Update the profile state to indicate an error - profileState := &objectcache.ProfileState{ - Error: fmt.Errorf("signature verification failed: %w", err), - } - apc.workloadIDToProfileState.Set(workloadID, profileState) - } - - // Update the profile in the cache - apc.workloadIDToProfile.Set(workloadID, fullProfile) - logger.L().Debug("added user-defined profile to cache", - helpers.String("containerID", containerID), - helpers.String("workloadID", workloadID), - helpers.String("namespace", container.K8s.Namespace), - helpers.String("profileName", userDefinedProfile)) + // verifyApplicationProfile already sets the profile state via setVerificationFailed + // Skip loading the profile into cache + } else { + // Update the profile in the cache only on successful verification + apc.workloadIDToProfile.Set(workloadID, fullProfile) + logger.L().Debug("added user-defined profile to cache", + helpers.String("containerID", containerID), + helpers.String("workloadID", workloadID), + helpers.String("namespace", container.K8s.Namespace), + helpers.String("profileName", userDefinedProfile)) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go` around lines 594 - 617, The code currently logs and records an error when apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile") fails but still proceeds to cache the unverified profile via apc.workloadIDToProfile.Set(workloadID, fullProfile); modify the control flow so that if verifyApplicationProfile returns an error you do not call apc.workloadIDToProfile.Set and instead skip adding fullProfile to the cache (e.g., return/continue or otherwise exit the enclosing block after setting workloadIDToProfileState), preserving the existing logging and profileState update logic.
♻️ Duplicate comments (3)
pkg/signature/cosign_adapter.go (3)
343-397:⚠️ Potential issue | 🔴 Critical
allowUntrusted=falsestill accepts attacker-supplied leaf certs.The strict path parses the certificate bundled in the annotations and immediately uses
cert.PublicKey, but it never verifies that certificate against any configured trust root or pinned key set. A modified object can be re-signed with a fresh self-signed leaf cert and still pass "strict" verification.#!/bin/bash set -euo pipefail sed -n '343,397p' pkg/signature/cosign_adapter.go printf '\n=== certificate-chain verification hooks ===\n' rg -n 'cert\.Verify|x509\.VerifyOptions|x509\.CertPool|fulcioroots\.Get|VerifySignedCertificateTimestamp' pkg/signature/cosign_adapter.goExpected result: the strict path loads
cert.PublicKeydirectly and does not perform certificate-chain/root validation before trusting it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 343 - 397, The strict path currently trusts the leaf certificate in the annotation (used when allowUntrusted is false) without performing certificate-chain or root verification; update the code around the block that calls sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256) (and the alternative public-key path using cryptoutils.UnmarshalPEMToPublicKey) to first build and verify the x509 chain: create x509.VerifyOptions with a root pool (e.g., fulcioroots.Get or configured CertPool), any intermediate pool, and appropriate DNS/Time checks, call cert.Verify(VerifyOptions), and only on successful verification proceed to LoadVerifier; if verification fails return an explicit error about untrusted certificate chain.
348-350:⚠️ Potential issue | 🟠 MajorValidate certificate lifetime at signing time, not wall-clock time.
Using
time.Now()makes valid keyless signatures start failing as soon as the short-lived Fulcio cert expires. Comparesig.TimestamptoNotBefore/NotAfterso historical verification still works.Proposed fix
- if time.Now().Before(cert.NotBefore) || time.Now().After(cert.NotAfter) { + signedAt := time.Unix(sig.Timestamp, 0) + if signedAt.Before(cert.NotBefore) || signedAt.After(cert.NotAfter) { return fmt.Errorf("certificate is not valid at this time") }When verifying Sigstore/Fulcio keyless signatures, should certificate validity be checked against the signing timestamp instead of the verifier's current wall-clock time?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 348 - 350, The certificate validity check uses time.Now(); change it to validate against the signature's signing time by comparing cert.NotBefore and cert.NotAfter to the signature timestamp (sig.Timestamp) instead of wall-clock time. Convert sig.Timestamp into a time.Time (e.g., time.Unix(sig.Timestamp, 0) or use the existing timestamp type) and replace the two time.Now().Before/After comparisons with comparisons against that signing time so historical verifications succeed; update the condition around cert, sig.Timestamp, and the error return accordingly.
129-150:⚠️ Potential issue | 🟠 MajorKeyless signing can't depend on browser auth or hard-coded claims.
The provider branch records fixed
identity/issuervalues, and the fallback opens an interactive OIDC flow from library code. That's already breaking CI, and it decouples the stored signer metadata from the actual token that Fulcio authenticated. Make the token source injectable/non-interactive and deriveidentity/issuerfrom the token claims.How does Sigstore/Fulcio keyless signing derive signer identity and issuer from an OIDC token, and is it valid to hard-code those fields or require an interactive browser fallback in headless CI?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 129 - 150, The code currently uses providers.Enabled/Provide to get tok but then hard-codes identity/issuer to sigstoreOIDC/sigstoreIssuer and falls back to oauthflow.OIDConnect (interactive) when no provider is enabled; replace this by making the token source injectable (accept a non-interactive token provider or token string from context/params instead of calling oauthflow.OIDConnect), stop using sigstoreOIDC/sigstoreIssuer constants, and parse/validate the returned OIDC token (the tok value) to extract claims for Subject and Issuer to populate identity and issuer; ensure functions that call this code (or the constructor) accept a TokenProvider or raw token so CI/headless environments can supply tokens without browser auth and the signer metadata is derived from the token claims rather than hard-coded values.
🧹 Nitpick comments (1)
pkg/signature/sign.go (1)
77-79:SignObjectWithKeyis misleading.This helper only disables keyless mode; when no
WithPrivateKeyoption is passed,NewCosignAdapter(false)generates a new keypair. For an offline-signing API, either accept a private key here or rename the helper to match what it actually does.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/sign.go` around lines 77 - 79, The helper SignObjectWithKey is misleading because it only sets WithKeyless(false) and still allows NewCosignAdapter(false) to generate a new keypair; fix by changing the helper to accept a private key parameter and pass it through to SignObject, e.g. change func SignObjectWithKey(obj SignableObject) to func SignObjectWithKey(obj SignableObject, privateKey string) error and call SignObject(obj, WithKeyless(false), WithPrivateKey(privateKey)); alternatively, if you prefer not to change the API, rename the function to reflect that it only disables keyless mode (e.g., SignObjectDisableKeyless) and update all callers and tests to match the new signature/name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 284-298: Replace the fragile string comparison of err.Error() in
the signature verification block (where signature.VerifyObject(profileAdapter)
is called) with errors.Is(err, signature.ErrObjectNotSigned) to detect a missing
signature reliably; update the conditional to use errors.Is and keep the same
Debug log for missing signatures and Warning (including helpers.Error(err)) for
other errors; ensure the package imports the standard errors package so
errors.Is is available.
In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-367: The merged NetworkNeighborhood must not retain signature
annotations from originalNN/fullUserNN; modify the merge path (e.g., in
performMerge or immediately after creating the mergedNN before any cache/store
call such as where nnc.workloadIDToProfileState.Set is invoked) to remove all
signature-related annotations (the keys used by signature.VerifyObjectStrict /
the signer: e.g., signature, signer public key, signing timestamp annotations)
from the merged object (or re-sign it) so downstream GetNetworkNeighborhood
cannot be mistaken for a valid signed object; ensure this stripping happens
after merge and before caching/storage.
In `@pkg/signature/cosign_adapter.go`:
- Around line 326-335: The VerifyData function currently dereferences
sig.Certificate and may panic if sig is nil; add an explicit nil-check at the
start of CosignAdapter.VerifyData (before calling pem.Decode) to return a
descriptive error when sig == nil, and likewise validate that sig.Certificate is
non-nil/empty and return an error instead of proceeding; update any error
messages to reference the Signature value to aid debugging (use the VerifyData
function name in the message).
In `@pkg/signature/profiles/adapter_test.go`:
- Around line 100-144: The test TestApplicationProfileAdapterSignAndVerify calls
signature.SignObjectKeyless which triggers an interactive OIDC flow and breaks
CI; either make the test skip in CI by checking os.Getenv("CI") and calling
t.Skipf(...) before invoking SignObjectKeyless, or change the test to use a
non-interactive path: replace signature.SignObjectKeyless(adapter) with
signature.SignObjectWithKey(adapter, testPrivateKey) using a generated in-test
key/fixture, then keep the subsequent checks and
signature.VerifyObjectStrict(adapter); update imports and add a small in-test
key generation helper if you choose the SignObjectWithKey route.
- Around line 232-274: TestSeccompProfileAdapterSignAndVerify currently calls
signature.SignObjectKeyless which is incompatible with CI; change the test to
perform a local/signing-key-based sign instead of keyless (e.g., replace
signature.SignObjectKeyless with the project’s local signing helper such as
SignObjectWithKey or a test signer/mocked signer) so the test can run offline,
keeping the rest of the flow (NewSeccompProfileAdapter, checking
profile.Annotations and signature.AnnotationSignature, and calling
signature.VerifyObjectStrict) unchanged; ensure the chosen signer produces the
same annotation key expected by VerifyObjectStrict.
In `@pkg/signature/profiles/rules_adapter_test.go`:
- Around line 117-120: The tests call signature.SignObjectKeyless(adapter) which
forces an interactive OIDC/browser flow; switch them to the non-interactive
keyed path by calling the keyed signing helper (e.g., signature.SignObject or
signature.SignObjectKeyed) with an ephemeral/test private key instead of
SignObjectKeyless, creating the test key inside the test (or reusing an existing
test helper) and passing it along with adapter; apply the same replacement for
both occurrences (the SignObjectKeyless calls at the two test sites) so the
adapter behavior is validated without invoking OIDC.
In `@pkg/signature/sign_test.go`:
- Around line 53-90: TestSignObjectKeyless triggers an interactive OIDC flow;
update the test to avoid interactive auth in CI by either skipping it when
running in CI (check env like CI or GITHUB_ACTIONS) or by injecting/mocking the
OIDC flow used by SignObjectKeyless so it returns a non-interactive,
deterministic response; modify TestSignObjectKeyless to detect CI and call
t.Skipf(...) or refactor the test to use a mock OIDC client/provider when
exercising SignObjectKeyless so the test runs headlessly and reliably in CI.
In `@pkg/signature/sign.go`:
- Around line 85-107: Both public helpers don't check for a nil SignableObject
and will panic; add a nil guard at the top of GetObjectSignature and IsSigned.
In GetObjectSignature (function GetObjectSignature) return a descriptive error
(e.g. "nil object") if obj == nil before calling obj.GetAnnotations(); in
IsSigned (function IsSigned) return false if obj == nil before accessing
GetAnnotations(); keep the existing annotations nil checks and existing behavior
for AnnotationSignature lookup.
---
Outside diff comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 594-617: The code currently logs and records an error when
apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile")
fails but still proceeds to cache the unverified profile via
apc.workloadIDToProfile.Set(workloadID, fullProfile); modify the control flow so
that if verifyApplicationProfile returns an error you do not call
apc.workloadIDToProfile.Set and instead skip adding fullProfile to the cache
(e.g., return/continue or otherwise exit the enclosing block after setting
workloadIDToProfileState), preserving the existing logging and profileState
update logic.
---
Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 343-397: The strict path currently trusts the leaf certificate in
the annotation (used when allowUntrusted is false) without performing
certificate-chain or root verification; update the code around the block that
calls sigstore_signature.LoadVerifier(cert.PublicKey, crypto.SHA256) (and the
alternative public-key path using cryptoutils.UnmarshalPEMToPublicKey) to first
build and verify the x509 chain: create x509.VerifyOptions with a root pool
(e.g., fulcioroots.Get or configured CertPool), any intermediate pool, and
appropriate DNS/Time checks, call cert.Verify(VerifyOptions), and only on
successful verification proceed to LoadVerifier; if verification fails return an
explicit error about untrusted certificate chain.
- Around line 348-350: The certificate validity check uses time.Now(); change it
to validate against the signature's signing time by comparing cert.NotBefore and
cert.NotAfter to the signature timestamp (sig.Timestamp) instead of wall-clock
time. Convert sig.Timestamp into a time.Time (e.g., time.Unix(sig.Timestamp, 0)
or use the existing timestamp type) and replace the two time.Now().Before/After
comparisons with comparisons against that signing time so historical
verifications succeed; update the condition around cert, sig.Timestamp, and the
error return accordingly.
- Around line 129-150: The code currently uses providers.Enabled/Provide to get
tok but then hard-codes identity/issuer to sigstoreOIDC/sigstoreIssuer and falls
back to oauthflow.OIDConnect (interactive) when no provider is enabled; replace
this by making the token source injectable (accept a non-interactive token
provider or token string from context/params instead of calling
oauthflow.OIDConnect), stop using sigstoreOIDC/sigstoreIssuer constants, and
parse/validate the returned OIDC token (the tok value) to extract claims for
Subject and Issuer to populate identity and issuer; ensure functions that call
this code (or the constructor) accept a TokenProvider or raw token so
CI/headless environments can supply tokens without browser auth and the signer
metadata is derived from the token claims rather than hard-coded values.
---
Nitpick comments:
In `@pkg/signature/sign.go`:
- Around line 77-79: The helper SignObjectWithKey is misleading because it only
sets WithKeyless(false) and still allows NewCosignAdapter(false) to generate a
new keypair; fix by changing the helper to accept a private key parameter and
pass it through to SignObject, e.g. change func SignObjectWithKey(obj
SignableObject) to func SignObjectWithKey(obj SignableObject, privateKey string)
error and call SignObject(obj, WithKeyless(false), WithPrivateKey(privateKey));
alternatively, if you prefer not to change the API, rename the function to
reflect that it only disables keyless mode (e.g., SignObjectDisableKeyless) and
update all callers and tests to match the new signature/name.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 86cd8613-f9a2-4577-b445-8ce2e443d7cf
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (28)
cmd/main.gocmd/sign-object/main.godocs/signing/README.mdgo.modpkg/config/config.gopkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.gopkg/rulemanager/ruleswatcher/watcher.gopkg/signature/annotations.gopkg/signature/cluster_flow_test.gopkg/signature/cluster_scenario_test.gopkg/signature/cosign_adapter.gopkg/signature/cosign_adapter_test.gopkg/signature/interface.gopkg/signature/profiles/adapter_test.gopkg/signature/profiles/applicationprofile_adapter.gopkg/signature/profiles/empty_typemeta_test.gopkg/signature/profiles/networkneighborhood_adapter.gopkg/signature/profiles/networkneighborhood_adapter_test.gopkg/signature/profiles/rules_adapter.gopkg/signature/profiles/rules_adapter_test.gopkg/signature/profiles/seccompprofile_adapter.gopkg/signature/sign.gopkg/signature/sign_test.gopkg/signature/signer.gopkg/signature/verifier.gopkg/signature/verify.gopkg/signature/verify_test.go
✅ Files skipped from review due to trivial changes (1)
- docs/signing/README.md
🚧 Files skipped from review as they are similar to previous changes (8)
- pkg/signature/profiles/networkneighborhood_adapter_test.go
- cmd/main.go
- pkg/signature/signer.go
- pkg/config/config.go
- pkg/signature/verify_test.go
- pkg/signature/cosign_adapter_test.go
- pkg/signature/profiles/rules_adapter.go
- pkg/signature/profiles/networkneighborhood_adapter.go
| err := signature.SignObjectKeyless(adapter) | ||
| if err != nil { | ||
| t.Fatalf("SignObjectKeyless failed: %v", err) | ||
| } |
There was a problem hiding this comment.
Keep these adapter tests off the interactive OIDC path.
signature.SignObjectKeyless makes these unit tests depend on the browser/OIDC flow, so they fail in CI before they actually validate the adapter behavior. Use the non-interactive keyed path here, and keep any real keyless coverage behind an explicit integration-test gate.
Suggested change
- err := signature.SignObjectKeyless(adapter)
+ err := signature.SignObjectWithKey(adapter)Apply the same swap in both tests.
Also applies to: 169-172
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/signature/profiles/rules_adapter_test.go` around lines 117 - 120, The
tests call signature.SignObjectKeyless(adapter) which forces an interactive
OIDC/browser flow; switch them to the non-interactive keyed path by calling the
keyed signing helper (e.g., signature.SignObject or signature.SignObjectKeyed)
with an ephemeral/test private key instead of SignObjectKeyless, creating the
test key inside the test (or reusing an existing test helper) and passing it
along with adapter; apply the same replacement for both occurrences (the
SignObjectKeyless calls at the two test sites) so the adapter behavior is
validated without invoking OIDC.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
pkg/signature/cosign_adapter.go (2)
331-340:⚠️ Potential issue | 🔴 CriticalGuard
VerifyDataagainst nilsigbefore dereferencing it.A nil
sigpanics at Line 339 (pem.Decode(sig.Certificate)) instead of failing closed. That turns malformed decode paths into a crash in the verification pipeline.🐛 Proposed fix
func (c *CosignAdapter) VerifyData(data []byte, sig *Signature, allowUntrusted bool) error { + if sig == nil { + return fmt.Errorf("VerifyData: signature is nil") + } + var verifier sigstore_signature.Verifier var err error🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 331 - 340, The VerifyData function risks a panic when sig is nil because it dereferences sig to call pem.Decode(sig.Certificate); add a nil-check at the start of VerifyData (in the CosignAdapter.VerifyData function) to return a clear error if sig == nil before any use of sig, and ensure downstream code (including the pem.Decode call and any uses of sig.Certificate or sig.PublicKey) only executes when sig is non-nil.
129-141:⚠️ Potential issue | 🟠 MajorDerive the signer identity from the returned OIDC token, not constants.
Lines 140-141 still hard-code
sigstoreOIDC/sigstoreIssuerafterproviders.Provide(...). Fulcio binds the certificate to the token’s real claims, andVerifyDatalater matchessig.Identityagainst that certificate. This can stamp incorrect metadata and make provider-path signatures fail verification for any non-matching subject/issuer.For Sigstore keyless signing with github.com/sigstore/cosign/v3/pkg/providers, should the signer identity and issuer be derived from the returned OIDC token claims instead of hard-coding them? Please point to the official Sigstore/Fulcio guidance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/signature/cosign_adapter.go` around lines 129 - 141, After calling providers.Provide(ctx, "sigstore") you must extract the actual OIDC token claims from the returned token and set identity and issuer from those claims instead of assigning the constants sigstoreOIDC/sigstoreIssuer; update the logic around providers.Provide -> tok (or the returned token structure) to decode the token claims (subject/sub and issuer/iss) and assign them to identity and issuer so that VerifyData and sig.Identity will match the Fulcio-bound certificate; modify the code paths that currently reference sigstoreOIDC and sigstoreIssuer to use the parsed claims from the token returned by providers.Provide and ensure any JWT/ID token parsing uses the token type returned by providers.Provide (or a simple JWT parser if necessary) to extract "sub" and "iss".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/objectcache/applicationprofilecache/applicationprofilecache.go`:
- Around line 596-600: The verification failure path currently returns nil
without updating the profile state; update the code in the block that calls
apc.verifyApplicationProfile(fullProfile, workloadID, "user-defined profile",
false) so that on error you set the profileState.Error (and any other relevant
fields like profileState.State) with the verification error before returning the
profileState (mirroring the fetch-failure handling). Ensure you reference and
update the same profileState variable used elsewhere in this function so
verification failures are recorded consistently for alerts/creator.go.
In `@pkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.go`:
- Around line 329-367: The current signature-failure branches set
workloadIDToProfileState and return but leave workloadIDToNetworkNeighborhood
containing a stale merged profile; when VerifyObjectStrict fails for the
originalNN (base-profile) you must evict nnc.workloadIDToNetworkNeighborhood for
toMerge.wlid (delete/Unset) before returning, and when VerifyObjectStrict fails
for fullUserNN (user-managed) after originalNN already verified you must restore
nnc.workloadIDToNetworkNeighborhood for toMerge.wlid to the originalNN value
(not the stale merged result) before returning; update the branches around
profiles.NewNetworkNeighborhoodAdapter/ signature.VerifyObjectStrict and use
workloadIDToNetworkNeighborhood.Set/Unset (or equivalent) referencing
toMerge.wlid, originalNN and fullUserNN to implement these fixes.
In `@pkg/signature/cosign_adapter.go`:
- Around line 388-394: The current check incorrectly ties Rekor bundle
enforcement to the adapter flag c.useKeyless; change the logic so strict
verification requires a Rekor bundle based on the signature's provenance not
adapter config — remove the dependency on c.useKeyless in the block that
inspects sig.RekorBundle and instead always treat a missing RekorBundle as a
strict-verification failure (or derive enforcement from the verification
request), so keyed signatures produced by signWithKey (which do not set
sig.RekorBundle) can succeed when allowed; update the conditional around
sig.RekorBundle (and any related error message) in the method containing that
logic and ensure NewCosignAdapter/NewCosignVerifier flows respect signature
provenance rather than c.useKeyless.
---
Duplicate comments:
In `@pkg/signature/cosign_adapter.go`:
- Around line 331-340: The VerifyData function risks a panic when sig is nil
because it dereferences sig to call pem.Decode(sig.Certificate); add a nil-check
at the start of VerifyData (in the CosignAdapter.VerifyData function) to return
a clear error if sig == nil before any use of sig, and ensure downstream code
(including the pem.Decode call and any uses of sig.Certificate or sig.PublicKey)
only executes when sig is non-nil.
- Around line 129-141: After calling providers.Provide(ctx, "sigstore") you must
extract the actual OIDC token claims from the returned token and set identity
and issuer from those claims instead of assigning the constants
sigstoreOIDC/sigstoreIssuer; update the logic around providers.Provide -> tok
(or the returned token structure) to decode the token claims (subject/sub and
issuer/iss) and assign them to identity and issuer so that VerifyData and
sig.Identity will match the Fulcio-bound certificate; modify the code paths that
currently reference sigstoreOIDC and sigstoreIssuer to use the parsed claims
from the token returned by providers.Provide and ensure any JWT/ID token parsing
uses the token type returned by providers.Provide (or a simple JWT parser if
necessary) to extract "sub" and "iss".
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6dbbf2b2-67d6-4939-af7b-9a442f427f20
📒 Files selected for processing (6)
pkg/objectcache/applicationprofilecache/applicationprofilecache.gopkg/objectcache/networkneighborhoodcache/networkneighborhoodcache.gopkg/signature/cluster_flow_test.gopkg/signature/cosign_adapter.gopkg/signature/profiles/networkneighborhood_adapter_test.gopkg/signature/verify.go
🚧 Files skipped from review as they are similar to previous changes (3)
- pkg/signature/cluster_flow_test.go
- pkg/signature/profiles/networkneighborhood_adapter_test.go
- pkg/signature/verify.go
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Implement generic signing and verification framework for ApplicationProfiles and SeccompProfiles using ECDSA P-256 signatures compatible with cosign.
Key features:
Core implementation:
Files:
Tests:
Overview
Summary by CodeRabbit
New Features
Behavior
Documentation
Tests